﻿using System;
using System.Collections.Concurrent;
using System.Reflection;
using System.Threading;
using Infrastructure.Logging;

namespace Infrastructure
{

    /// <summary>
    /// Thread Manager is an extension to <see cref="ThreadPool"/> that allowes you to manage how many
    /// work items are pending in the queue and how many cuncurent items are beeing executed.
    /// <remarks>
    /// <para>
    /// Underneath <see cref="ThreadPool"/> is used to execute each work item async and a manager thread is used
    /// to devide work so only a limited number of concurent work is been done
    /// </para>
    /// <para>
    /// <div>There are two ways to use the Thread Manager:</div>
    /// <div>1. using instance: subscribering to <see cref="HandleWorkEvent"/> event</div>
    /// <div>2. using inheritance: overwriting <see cref="HandleWork"/> method</div>
    /// <div>In both ways the event\method is executed concurrently on a <see cref="ThreadPool"/> thread so you
    /// must handle the synchronization if required.</div>
    /// <div>additionally the <see cref="ThreadPool"/> number of thread is limited, so if you
    /// require more threads than the thread pool currently has (default: 25) the bouncded worker will extend
    /// the amount by calling <see cref="ThreadPool.SetMaxThreads"/>.</div>
    /// </para>
    /// <para>
    /// The worker has a dedicated thread to handle the work distribution, it's not the best for performance so if you
    /// want to use many Thread Managers in your application you should review this and make it better.
    /// </para>
    /// </remarks>
    /// </summary>
    /// <typeparam name="T">the type of the work item</typeparam>
    public class ThreadsManager<T> : IDisposable
    {
        #region Data members

        /// <summary>
        /// logger
        /// </summary>
        private static readonly ILog log = LogManager.GetLogger( MethodBase.GetCurrentMethod().DeclaringType );

        /// <summary>
        /// queue of bounces to handle
        /// </summary>
        private readonly BlockingCollection<Handle> _queue;

        /// <summary>
        /// collection of tokens to lmit the concurent workers 
        /// </summary>
        protected readonly BlockingCollection<object> _tokens;

        /// <summary>
        /// work distribution thread of the handler
        /// </summary>
        private Thread _workDistributionThread;

        /// <summary>
        /// worker thread of the handler
        /// </summary>
        /// <remarks>
        /// we create one worker thread to be always avalible as a work around 
        /// to be sure the Thread Manager wont be stucked because of lack of thread in the threadpool
        /// 
        /// it happened that all the thread in the thread pool tried to insert work to Thread Manager,
        /// and when the queue was full they were blocked (and therfore occupied all the threadpool threads)
        /// so the Thread Manager couldn't accomplished any job so some kind of stravasion deadlock occured
        /// 
        /// i think we should use private threadpools when there is more than x tokens
        /// </remarks>
        private Thread _workerThread;

        /// <summary>
        /// flag to indicate if to keep alive the handler
        /// </summary>
        private bool _keepAlive = true;

        #endregion

        /// <summary>
        /// Raised on work item handle request, when an work item has a free worker to process it.
        /// <div>if you use the derived option, do not use this event</div>
        /// </summary>
        public event Action<Handle> HandleWorkEvent;

        /// <summary>
        /// Init bounced worker with <paramref name="queueLimit"/> and <paramref name="workerLimit"/>.
        /// <div>the worker is not started until <see cref="StartAsync"/> is executed</div>
        /// </summary>
        /// <param name="queueLimit">the max number of work items allowed in the queue</param>
        /// <param name="workerLimit">the max numbers of cuncurent workers allowed</param>
        public ThreadsManager ( int queueLimit, int workerLimit )
        {
            _queue = new BlockingCollection<Handle>( queueLimit );
            _tokens = new BlockingCollection<object>( workerLimit );

            int workerThreads, ioThreads;
            ThreadPool.GetMaxThreads( out workerThreads, out ioThreads );
            if (workerLimit > workerThreads)
                ThreadPool.SetMaxThreads( workerLimit + 5, ioThreads );

            for (int i = 0; i < _tokens.BoundedCapacity; i++)
                _tokens.Add( new object() );
        }

        /// <summary>
        /// Get the number of work items currently in the queue.
        /// </summary>
        public int WorkCount
        {
            get { return _queue.Count; }
        }

        /// <summary>
        /// Get the number of worker currently active
        /// </summary>
        public int ActiveWorkersCount
        {
            get { return _tokens.BoundedCapacity - _tokens.Count; }
        }

        /// <summary>
        /// return true if Thread Manager is idle
        /// </summary>
        public bool IsIdle
        {
            get { return _queue.Count == 0 && ActiveWorkersCount == 0; }
        }

        /// <summary>
        /// Start working thread and folder listener.
        /// <div>if overriden must call base first</div>
        /// </summary>
        /// <param name="useSingleNonThreadPoolThread">true: one worker threaed will be real thread and not thread pool thread, use
        /// it to prevent dead locking on thread pool</param>
        public virtual void StartAsync ( bool useSingleNonThreadPoolThread = false )
        {
            if (useSingleNonThreadPoolThread)
            {
                _workerThread = new Thread( InfiniteWorkerMethod );
                _workerThread.IsBackground = true;
                _workerThread.Start();
            }

            _workDistributionThread = new Thread( WorkDistributionLoop );
            _workDistributionThread.IsBackground = true;
            _workDistributionThread.Start();
        }

        /// <summary>
        /// stop working thread and folder listener.
        /// <div>doesn't ait for the work to finish, queued work will not be executed</div>
        /// <div>if overriden must call base first</div>
        /// </summary>
        public virtual void Stop ()
        {
            _keepAlive = false;
            _queue.CompleteAdding();
            _workDistributionThread = null;
        }

        /// <summary>
        /// Wait for the worked to finish all it's work.
        /// <div>there still can be items added</div>
        /// <div>the call is blocked until <see cref="IsIdle"/> is true</div>
        /// </summary>
        /// <param name="timeout">optional: the max amount of time to wait for finish (default: null, forever)</param>
        /// <returns>true - worker has finished, false - timeout reached first</returns>
        public bool WaitToFinish ( TimeSpan? timeout = null )
        {
            var endTime = timeout.HasValue ? DateTime.Now.Add( timeout.Value ) : DateTime.Now.AddYears( 1 );
            while (!IsIdle && DateTime.Now < endTime)
            {
                Thread.Sleep( 500 );
            }
            return timeout != null && IsIdle;
        }

        /// <summary>
        /// Add work item to the queue.
        /// <div>if the queue if full the call is blocked</div>
        /// <div>generaly similar behavior as <see cref="ThreadPool.QueueUserWorkItem(System.Threading.WaitCallback)"/></div>
        /// </summary>
        /// <param name="workItem">the work item to add</param>
        public Handle InsertWorkItem ( T workItem, TimeSpan useIn = new TimeSpan() )
        {
            log.Info( "Insert work item..." );
            var handle = new Handle( workItem, useIn );
            _queue.Add( handle );
            return handle;
        }

        /// <summary>
        /// Add work item to the queue.
        /// <div>if the queue if full the call is blocked</div>
        /// <div>generaly similar behavior as <see cref="ThreadPool.QueueUserWorkItem(System.Threading.WaitCallback)"/></div>
        /// </summary>
        /// <param name="workItem">the work item to add</param>
        public void InsertWorkItemAsync ( T workItem, TimeSpan useIn = new TimeSpan() )
        {
            log.Info( "Insert work item async..." );
            var workItemToAdd = new Handle( workItem, useIn );
            if (!_queue.TryAdd( workItemToAdd, 0 ))
                ThreadPool.QueueUserWorkItem( s => _queue.Add( workItemToAdd ) );
        }

        /// <summary>
        /// infinite worker method
        /// </summary>
        private void InfiniteWorkerMethod ()
        {
            while (_keepAlive)
            {
                if (_queue.Count > 0)
                {
                    object token = _tokens.Take();
                    WorkerMethod( token, 100 );
                }
                else
                {
                    Thread.Sleep( 100 );
                }
            }
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="token"></param>
        /// <param name="timeout"></param>
        private void WorkerMethod ( object token, int timeout )
        {
            try
            {
                Handle workItem;
                if (_queue.TryTake( out workItem, timeout ))
                {
                    if (DateTime.Now -workItem._createdDate > workItem._useIn)
                    {
                        HandleWork( workItem );
                    }
                    else
                    {
                        Thread.Sleep( 200 );
                        _queue.Add( workItem );
                    }
                }
            }
            catch (Exception e)
            {
                log.Error( "Unhandled Exception occured while doing some work", e );
            }
            finally
            {
                _tokens.Add( token );
            }
        }

        /// <summary>
        /// Run when works is pending to be executed.
        /// <div>the method is run on <see cref="ThreadPool"/> thread</div>
        /// <div>this method will not execute concurrently more than the defined workLimit</div>
        /// <div>by default the methods invokes the <see cref="HandleWorkEvent"/> event</div>
        /// </summary>
        /// <param name="workItem">the work item to handle</param>
        protected virtual void HandleWork ( Handle workItem )
        {
            if (HandleWorkEvent != null)
                HandleWorkEvent( workItem );
        }

        /// <summary>
        /// same as <see cref="Stop"/>
        /// </summary>
        public virtual void Dispose ()
        {
            Stop();
        }

        /// <summary>
        /// The work loop of the worker thread that handles the work distribution.
        /// <div>Each iteration the loop tries to start a new work item in the queue, if there is no
        /// work it will be blocked (waste of thread) until a new item is inserted. If there are no free
        /// work token it will be blocked again until some work is finished so a work item is available.</div>
        /// </summary>
        private void WorkDistributionLoop ()
        {
            log.Info( "WorkerLoop Thread Manager..." );

            try
            {
                while (_keepAlive)
                {
                    //if there is work and can assign new worker...do it
                    if (_queue.Count > 0)
                    {
                        // get token so no more workers than token can execute at once
                        object token = _tokens.Take();

                        // handle work
                        // as long as we use public threadpool the action must be as short as possible
                        // (better to queue 10 jobs and not performing them all without releasing the thread to other processes)
                        ThreadPool.QueueUserWorkItem( state => WorkerMethod( token, 100 ) );
                    }
                    else
                    {
                        Thread.Sleep( 100 );
                    }
                }
            }
            catch (OperationCanceledException)
            { }
            catch (ThreadAbortException)
            { }
            catch (Exception ex)
            {
                log.Error( "Thread Manager failed", ex );
            }

            log.Info( "Thread Manager finished." );
        }

        public class Handle
        {
            internal readonly TimeSpan _useIn;
            internal readonly DateTime _createdDate;

            public T Item { get; private set; }

            public bool HandleItem { get; set; }

            internal Handle ( T item, TimeSpan useIn )
            {
                _useIn = useIn;
                Item = item;
                _createdDate = DateTime.Now;
            }
        }
    }
}
